package at.ac.tuwien.swa.sparrow.server.component

import ac.at.tuwien.infosys.swa.audio.Fingerprint
import at.ac.tuwien.swa.sparrow._
import at.ac.tuwien.swa.sparrow.common._
import at.ac.tuwien.swa.sparrow.peer.PeerInterface
import at.ac.tuwien.swa.sparrow.server.persistence._
import at.ac.tuwien.swa.sparrow.server.security.SecurityContext
import com.google.common.net.HostAndPort
import com.typesafe.scalalogging.log4j.Logging
import java.util.{TimerTask, Timer, UUID}
import javax.inject.Inject
import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean
import org.springframework.stereotype.Component
import scala.collection.mutable
import scala.concurrent.duration._
import scala.util.{Success, Failure, Random}

@Component
class PeerManager @Inject()(billingEngine: BillingEngine, ticketProvider: TicketProvider,
														accountRepo: AccountRepository, peerRepo: PeerRepository,
														recognitionRepo: SongRecognitionRepository, trackRepo: TrackRepository) extends Logging {

	val tickets = new mutable.HashMap[Ticket, Option[TrackInfo]]() with mutable.ObservableMap[Ticket, Option[TrackInfo]]
	val timer = new Timer(true)

	// Periodically check whether a peer terminated unexpectedly
	timer.scheduleAtFixedRate(new TimerTask {
		def run() {
			for {
				t <- tickets
				if t._2.isEmpty
				if !t._1.isExpired
			} {
				onlinePeers find (_.address == t._1.peers.lastOption.orNull) match {
					case Some(_) =>
					case None => logger.info(s"Continuing with ticket ${t._1} (last peer was ${t._1.peers.lastOption.getOrElse("none")}}).")
						request(t._1)
				}
			}
		}
	}, 10.seconds.toMillis, 10.seconds.toMillis)

	def register(username: String, password: String) = accountRepo findOne(username, password) match {
		case None => throw AccessDeniedException()
		case Some(a: Account) => peerRepo.save(Peer(UUID.randomUUID, a, null, 0, false)).peerId
	}

	def login(peerId: UUID, address: HostAndPort): Peer = peerRepo findOne (p => p._2.peerId == peerId) match {
		case None => throw AccessDeniedException()
		case Some(p) => {
			p.update(address)
			update(PeerInfo(p.id, p.address, p.lastOnline, p.online))
			p
		}
	}

	def logout(peerId: UUID) {
		peerRepo findOne (_._2.peerId == peerId) match {
			case None =>
			case Some(p) =>
				logger.info(s"Peer $p logged out.")
				p.update(p.address, false)
				update(PeerInfo(p.id, p.address, p.lastOnline, p.online))
		}
	}

	def update(peerInfo: PeerInfo) {
		peerRepo.findOne(_._2.id == peerInfo.id) match {
			case Some(p) if p.lastOnline <= peerInfo.lastOnline => p.update(peerInfo.address, peerInfo.online, peerInfo.lastOnline)
			case _ => logger.debug("Ignoring outdated peer information: " + peerInfo)
		}
	}

	def update(ticket: Ticket, trackInfo: Option[TrackInfo]) {
		if (trackInfo.isDefined) {
			tickets += (ticket -> trackInfo)
			val metadata = trackRepo.save(new TrackMetadata(trackInfo.get))
			recognitionRepo.save(SongRecognition(metadata))
			billingEngine.pay(ticket.userId, ticket)
			SecurityContext.getRole match {
				case p: Peer if p.account.id != ticket.userId => p.account.credits.addAndGet(SEARCH_REWARD)
				case _ =>
			}
		} else if ((tickets applyOrElse(ticket, (value: Ticket) => trackInfo)).isEmpty) {
			tickets += (ticket -> trackInfo)
			SecurityContext.getRole match {
				case p: Peer if ticket.peers.last == p.address => request(ticket)
				case _ =>
			}
		}
	}

	def request(authToken: AuthToken, fp: Fingerprint): Ticket = request(ticketProvider create(authToken.id, fp))

	def request(ticket: Ticket): Ticket = {
		val candidates = onlinePeers filterNot (ticket.peers contains _.address)
		Random shuffle candidates collectFirst {
			case p if !ticket.isExpired => val r = connect(p.address).request(ticket)
				r match {
					case Failure(e) => logger.info(s"Got exception while connecting to peer ${p.address}", e)
					case Success(t) => logger.info(s"Forwarded request to peer ${p.address}")
						t.peers += p.address
						t
				}
				r.get
		} match {
			case Some(t) => tickets += (t -> None); t
			case None if !ticket.isValid => {
				logger.info(s"Got invalid ticket $ticket. Aborting...")
				tickets -= ticket
				tickets += (ticket -> None)
				ticket
			}
			case None => {
				val copy = ticket.copy(hopCount = ticket.peers.size)
				logger.info(s"Running out of peers. Ticket $ticket is not processable.")
				tickets -= ticket
				tickets += (copy -> None)
				billingEngine.pay(ticket.userId, copy)
 				copy
			}
		}
	}

	def onlinePeers = (peerRepo findAll (p => p._2.online
		&& System.currentTimeMillis - p._2.lastOnline < (PEER_TIMEOUT + 10.seconds).toMillis)).toSet

	private def connect(address: HostAndPort) = {
		logger.debug(s"Attempting to connect to peer $address")
		val httpInvokerProxy = new HttpInvokerProxyFactoryBean
		httpInvokerProxy.setServiceUrl(s"http://$address/remoting/PeerInterface")
		httpInvokerProxy.setServiceInterface(classOf[PeerInterface])
		httpInvokerProxy.afterPropertiesSet()
		httpInvokerProxy.getObject.asInstanceOf[PeerInterface]
	}
}
